﻿// Deep Earth is a community project available under the Microsoft Public License (Ms-PL)
// Code is provided as is and with no warranty – Use at your own risk
// View the project and the latest code at http://DeepEarth.codeplex.com/

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using DeepEarth.Client.MapControl.Events;
using DeepEarth.Client.MapControl.Layers;
using GeoAPI.Geometries;

namespace DeepEarth.Client.MapControl.Geometries
{
    public class GeometryLayer : Layer, ICollection<GeometryControlBase<IGeometry>>
    {
        private readonly Map _map;
        private readonly double _updatesPerSecond;
        private static int _topZ = 1000;
        private static int _lowZ = -1000;

        private readonly Dictionary<Guid, GeometryControlBase<IGeometry>> _items;

        /// <summary>
        /// Sets or gets the Style property to be applied to all geometry items in the layer
        /// </summary>
        public Style ItemsStyle { get; set; }

        /// <summary>
        /// Represents the technique used to update the location of child geometry items.
        /// </summary>
        public UpdateModes UpdateMode
        {
            get { return _updateMode; }
            set
            {
                _updateMode = value;

                switch (_updateMode)
                {
                    case UpdateModes.ElementUpdate:
                        {
                            break;
                        }

                    case UpdateModes.PanOnlyUpdate:
                        {
                            InitLayerTransforms();
                            UpdateTranslateTransform();
                            break;
                        }

                    case UpdateModes.TransformUpdate:
                        {
                            InitLayerTransforms();
                            UpdateLayerTranforms();
                            break;
                        }
                }
            }
        }
        private UpdateModes _updateMode;

        public GeometryLayer(Map map)
        {
            _map = map;

            if (!DesignerProperties.IsInDesignTool)
            {
                _map.GetMapViewChanged().Subscribe(ViewChanged);
                _map.GetMapZoomStarted().Subscribe(ZoomStarted);
                _map.GetMapZoomEnded().Subscribe(ZoomEnded);
                _map.GetMapLoaded().Subscribe(MapLoaded);

                if (_sbAnimation == null)
                {
                    _sbAnimation = new Storyboard { Duration = new Duration(TimeSpan.FromSeconds(_updatesPerSecond)) };
                    _sbAnimation.GetCompleted().Subscribe(SbAnimationCompleted);
                }

                _updatesPerSecond = 0;
                _priorToZoomVisibility = Visibility.Visible;

                _items = new Dictionary<Guid, GeometryControlBase<IGeometry>>();
            }
        }

        /// <summary>
        /// Since geometry items may have been added before map wad loaded, this event handler will
        /// iterate its children and assure all geometries are loaded onto the Canvas
        /// </summary> 
        /// <param name="event">Map EventArgs</param>
        private void MapLoaded(IEvent<MapEventArgs> @event)
        {
            //Make sure that all shapes are on the canvas not that the map is ready
            UpdateView();
            foreach (var shape in _items.Values)
            {
                if (Children.Contains(shape) == false)
                {
                    Children.Add(shape);
                    UpdateChildLocation(shape);
                }
            }
        }

        /// <summary>
        /// Indexer for accessing child geometry items by index location.
        /// </summary>
        /// <param name="id">Unique identifier for the shape</param>
        public GeometryControlBase<IGeometry> this[Guid id]
        {
            get
            {
                return _items.Values.FirstOrDefault(shape => shape.Id == id);
            }
        }

        /// <summary>
        /// The enumerable collection of geometries for the GeometryLayer
        /// </summary>
        public IEnumerable<GeometryControlBase<IGeometry>> Geometries
        {
            get
            {
                return _items.Values;
            }
        }

        /// <summary>
        /// The enumerable collection of GeometryBase objects that are within the geographic bounds of the Map
        /// </summary>
        public IEnumerable<GeometryControlBase<IGeometry>> GeometryInView
        {
            get
            {
                Rect bounds = _map.GeoBounds;
                return _items.Values.Where(shape => shape.Intersects(bounds));
            }
        }

        /// <summary>
        /// Get the specified shape by its ID
        /// </summary>
        /// <param name="id">The GUID ID of the shape</param>
        /// <returns>GeometryBase object representing the shape, null if nothing is found</returns>
        public GeometryControlBase<IGeometry> GetShapeById(Guid id)
        {
            GeometryControlBase<IGeometry> item;
            _items.TryGetValue(id, out item);
            return item;
        }

        /// <summary>
        /// Move the specified geometry to the top (max Z index) of the canvas
        /// </summary>
        /// <param name="item">The geometry to move</param>
        public void SendToTop(GeometryControlBase<IGeometry> item)
        {
            SetZIndex(item, _topZ++);
        }

        /// <summary>
        /// Move the specified geometry to the bottom (min Z index) of the canvas
        /// </summary>
        /// <param name="item">The geometry to move</param>
        public void SendToBack(GeometryControlBase<IGeometry> item)
        {
            SetZIndex(item, _lowZ--);
        }

        /// <summary>
        /// Handles the visibility change event
        /// </summary>
        protected override void OnIsVisibleChanged()
        {
            base.OnIsVisibleChanged();
            UpdateAllShapes();
        }


        #region ICollection Members

        public virtual int Count
        {
            get { return _items.Count; }
        }

        public virtual bool IsReadOnly
        {
            get { return false; }
        }

        public IEnumerator<GeometryControlBase<IGeometry>> GetEnumerator()
        {
            //foreach (var item in _items.Values)
            //{
            //    yield return item;
            //}

            return ((IEnumerable<GeometryControlBase<IGeometry>>)_items.Values).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (IEnumerator)new GeometryLayer(_map);
            // return GetEnumerator();
        }

        public void Add(GeometryControlBase<IGeometry> item)
        {
            item.Layer = this;
            _items.Add(item.Id, item);

            // TODO: Get styles for Geometries from Generic.xaml
            //Apply a System.Windows.Style if one is set on the layer
            if (ItemsStyle != null) item.Style = ItemsStyle;

            if (_map.IsMapLoaded)
            {
                Children.Add(item);

                //If we are in TransformMode update the shape whether it is in view or not.
                switch (UpdateMode)
                {
                    case UpdateModes.PanOnlyUpdate:
                        {
                            break;
                        }

                    case UpdateModes.TransformUpdate:
                        {
                            UpdateChildLocation(item);
                            break;
                        }

                    case UpdateModes.ElementUpdate:
                        {
                            UpdateShapeView(item, _map.GeoBounds);
                            break;
                        }
                }
            }
        }

        public void Clear()
        {
            var items = new GeometryControlBase<IGeometry>[_items.Count];
            CopyTo(items, 0);

            foreach (var item in items)
            {
                // RemoveShape(item);
                Remove(item);
            }

            // TODO: ADD items.Clear();
            // items.Clear(items, 0, items.Length);
        }

        //private void RemoveShape(GeometryBase<IGeometry> item)
        //{

        //}

        public bool Contains(GeometryControlBase<IGeometry> item)
        {
            return _items.ContainsValue(item);
        }

        public void CopyTo(GeometryControlBase<IGeometry>[] items, int index)
        {
            _items.Values.CopyTo(items, index);
        }

        public bool Remove(GeometryControlBase<IGeometry> item)
        {
            _items.Remove(item.Id);
            if (Children.Contains(item))
            {
                Children.Remove(item);
                return true;
            }
            return false;
        }

        #endregion


        #region Canvas Transformation Operations

        private void InitLayerTransforms()
        {
            var transformGroup = new TransformGroup();
            var translateTransform = new TranslateTransform { X = 1, Y = 1 };
            var scaleTransform = new ScaleTransform { ScaleX = 1.0, ScaleY = 1.0 };

            //var compositeTransform = new CompositeTransform();

            //compositeTransform.CenterX = 1;
            //compositeTransform.CenterY = 1;

            //compositeTransform.ScaleX = 1.0;
            //compositeTransform.ScaleY = 1.0;

            //transformGroup.Children.Add(compositeTransform);

            //RenderTransform = transformGroup;

            switch (UpdateMode)
            {
                case UpdateModes.TransformUpdate:
                    {
                        //This Size is arbitrary, but we'll set it to Max Map Size as this becomes the basis for scaling child elements.
                        Width = 67108864;
                        Height = 67108864;
                        break;
                    }
            }

            transformGroup.Children.Add(scaleTransform);
            transformGroup.Children.Add(translateTransform);
            RenderTransform = transformGroup;
        }

        private void UpdateLayerTranforms()
        {
            if (RenderTransform != null)
            {
                UpdateScaleTransform();
                UpdateTranslateTransform();
            }

        }

        private void UpdateTranslateTransform()
        {
            if (RenderTransform != null)
            {
                var offset = ViewOffset;
                var translate = (TranslateTransform)((TransformGroup)RenderTransform).Children[1];
                translate.X = -offset.X;
                translate.Y = -offset.Y;
            }
        }

        private void UpdateScaleTransform()
        {
            if (RenderTransform != null)
            {
                var scaleFactor = MapExtentToFullSizeScale;

                var scale = (ScaleTransform)((TransformGroup)RenderTransform).Children[0];
                scale.ScaleX = scaleFactor.X;
                scale.ScaleY = scaleFactor.Y;

                //var compositeTransform = ((CompositeTransform)RenderTransform);
                //compositeTransform.ScaleX = scaleFactor.X;
                //compositeTransform.ScaleY = scaleFactor.Y;

                //Update scale of children on canvas
                double scaleAdjustment = 1 / scaleFactor.X;
                foreach (var element in Children)
                {
                    if (element is GeometryControlBase<IGeometry>)
                    {
                        var geometry = element as GeometryControlBase<IGeometry>;
                        geometry.ScaleAdjustment = scaleAdjustment;
                    }
                }
            }
        }

        private void UpdateScale(GeometryControlBase<IGeometry> geometry)
        {
            if (UpdateMode == UpdateModes.TransformUpdate)
            {
                if (MapInstance.IsMapLoaded)
                {
                    Point scaleFactor = MapExtentToFullSizeScale;
                    double scaleAdjust = 1 / scaleFactor.X;
                    geometry.ScaleAdjustment = scaleAdjust;
                }
            }
        }

        internal Point MapExtentToFullSizeScale
        {
            get
            {
                if (_map != null)
                {
                    Size mapExtent = _map.MapExtentPixelSize;
                    double xScale = mapExtent.Width / Width;
                    double yScale = mapExtent.Height / Height;
                    return new Point(xScale, yScale);
                }
                return new Point(0, 0);
            }
        }

        private Point ViewOffset
        {
            get
            {
                var point = new Point();
                if (_map.IsMapLoaded)
                {
                    switch (UpdateMode)
                    {
                        case UpdateModes.PanOnlyUpdate:
                            {
                                break;
                            }

                        case UpdateModes.TransformUpdate:
                            {
                                Size mapExtent = _map.MapExtentPixelSize;
                                point = new Point(_map.LogicalOrigin.X * mapExtent.Width, _MapInstance.LogicalOrigin.Y * mapExtent.Height);
                                break;
                            }

                        default:
                            {
                                point = new Point();
                                break;
                            }
                    }
                }
                else
                {
                    point = new Point();
                }
                return point;
            }
        }

        #endregion


        #region Animation Update Loop Operations

        private const double ChangeSignificanceThreshold = 0.05;
        private bool _inAnimation;
        private bool _inMotion;
        private Point _priorOrigin;


        // make the animation last 1500 ms, which is the length of the animation in Deep Zoom
        private TimeSpan _animationDuration = new TimeSpan(0, 0, 0, 0, 1500);
        private Storyboard _sbAnimation;
        private DateTime _updateStartTime;
        private bool _inPanOnlyZoom;
        private Visibility _priorToZoomVisibility;

        private void UpdateView()
        {
            switch (UpdateMode)
            {
                case UpdateModes.PanOnlyUpdate:
                    UpdateTranslateTransform();
                    break;

                case UpdateModes.TransformUpdate:
                    UpdateLayerTranforms();
                    break;

                case UpdateModes.ElementUpdate:
                    _inMotion = true;

                    //Ignore if we are already in process of an update.
                    if (_inAnimation == false)
                    {
                        _updateStartTime = DateTime.Now;
                        if (_sbAnimation == null)
                        {
                            _sbAnimation = new Storyboard { Duration = new Duration(TimeSpan.FromSeconds(_updatesPerSecond)) };
                            _sbAnimation.GetCompleted().Subscribe(SbAnimationCompleted);
                        }
                        _sbAnimation.Begin();
                    }
                    break;
            }
        }


        private void SbAnimationCompleted(IEvent<EventArgs> @event)
        {
            //System.Diagnostics.Debug.WriteLine(string.Format("UpdateCounter: {0}", counter++));

            double progress = (DateTime.Now - _updateStartTime).TotalSeconds / _animationDuration.TotalSeconds;
            if (progress >= 1)
            {
                //Clean Up. Final UpdateShapes moved to MotionComplete event handler
                _sbAnimation = null;
                _inAnimation = false;
            }
            else
            {
                //Uses the sum of squares to determine distance moved.
                //The wobble effect seems to occur at ~ a .03% of the logical display.  
                //So setting the change tolerance at .05 eliminates much of the wobble effect especially post zoom.
                double distLogChange = Math.Sqrt(Math.Pow(_map.LogicalOrigin.X - _priorOrigin.X, 2) + Math.Pow(_map.LogicalOrigin.Y - _priorOrigin.Y, 2));
                double changeSignificance = 100 * distLogChange / _map.MapViewLogicalSize.Width;

                if (changeSignificance > ChangeSignificanceThreshold)
                {
                    _priorOrigin = _map.LogicalOrigin;
                    UpdateAllShapes();
                }

                //Provide a short circuit if MSI.MotionEnded has been called.
                if (_inMotion)
                {
                    //Loop until time is up.
                    _inAnimation = true;
                    _sbAnimation.Begin();
                }
                else
                {
                    _sbAnimation = null;
                    _inAnimation = false;
                }
            }
        }

        private void ViewChanged(IEvent<MapEventArgs> @event)
        {
            UpdateView();
        }

        private void ZoomStarted(IEvent<MapEventArgs> @event)
        {
            if (UpdateMode == UpdateModes.PanOnlyUpdate)
            {
                if (_inPanOnlyZoom == false)
                {
                    _inPanOnlyZoom = true;
                    _priorToZoomVisibility = Visibility;
                    Visibility = Visibility.Collapsed;
                }
            }
        }

        private void ZoomEnded(IEvent<MapEventArgs> @event)
        {
            _inMotion = false;

            switch (UpdateMode)
            {
                case UpdateModes.ElementUpdate:
                    {
                        UpdateAllShapes();
                        break;
                    }
                case UpdateModes.PanOnlyUpdate:
                    {
                        _inPanOnlyZoom = false;
                        Visibility = _priorToZoomVisibility;
                        UpdateAllShapes();
                        //UpdateTranslateTransform();
                        break;
                    }
                case UpdateModes.TransformUpdate:
                    {
                        //No update needed
                        break;
                    }
            }
        }

        #endregion


        #region Canvas Update Operations

        /// <summary>
        /// Update the location of all child shapes.
        /// </summary>
        internal void UpdateAllShapes()
        {
            if (_items.Count > 0)
            {
                Rect bounds = _map.GeoBounds;
                var shapesCopy = new GeometryControlBase<IGeometry>[_items.Count];
                _items.Values.CopyTo(shapesCopy, 0);

                foreach (var geometry in shapesCopy)
                {
                    UpdateShapeView(geometry, bounds);
                }
            }
        }

        /// <summary>
        /// Update the location of a particular shape.
        /// </summary>
        /// <param name="item"></param>
        internal void UpdateShape(GeometryControlBase<IGeometry> item)
        {
            UpdateShapeView(item, _map.GeoBounds);
        }

        /// <summary>
        /// Update the location of the shape if it is within the specified extent
        /// </summary>
        /// <param name="item">The geometry to update</param>
        /// <param name="bounds">The extent to intersect</param>
        private void UpdateShapeView(GeometryControlBase<IGeometry> item, Rect bounds)
        {
            if (_map.IsMapLoaded)
            {
                if (item.Layer.IsVisible)
                {
                    if (item.IsVisible)
                    {
                        if (item.Intersects(bounds))
                        {
                            ShowShape(item);
                        }
                        else
                        {
                            HideShape(item);
                        }
                    }
                }
                else
                {
                    HideShape(item);
                }
            }
        }

        ///<summary>
        /// Updates the location of the shape on the canvas with respect to configured update mode.
        ///</summary>
        ///<param name="item">Shape which needs its canvas location updated.</param>
        public void UpdateChildLocation(GeometryControlBase<IGeometry> item)
        {
            if (item.IsLoaded)
            {
                if (UpdateMode == UpdateModes.TransformUpdate)
                {
                    UpdateScale(item);
                }

                if (item is PointControl)
                {
                    UpdatePoint((PointControl)item);
                }

                if (item is PathControlBase)
                {
                    UpdatePathData((PathControlBase)item);
                }
            }
        }


        private void UpdatePoint(PointControl dp)
        {
            switch (UpdateMode)
            {
                case UpdateModes.PanOnlyUpdate:
                    {
                        Size mapExtent = MapInstance.MapExtentPixelSize;
                        Point logicalPoint = dp.LogicalCoordinate;
                        Point pixel = new Point(logicalPoint.X * mapExtent.Width, logicalPoint.Y * mapExtent.Height);
                        SetLeft(dp, pixel.X - dp.Anchor.X);
                        SetTop(dp, pixel.Y - dp.Anchor.Y);
                        break;
                    }
                case UpdateModes.ElementUpdate:
                    {
                        Point pixel = Map.DefaultInstance.CoordHelper.LogicalToPixel(dp.LogicalCoordinate);
                        SetLeft(dp, pixel.X - dp.Anchor.X);
                        SetTop(dp, pixel.Y - dp.Anchor.Y);
                        break;
                    }

                case UpdateModes.TransformUpdate:
                    {
                        Size mapExtent = MapInstance.MapExtentPixelSize;
                        double xMaxToCurrRatio = Width / mapExtent.Width;
                        double yMaxToCurrRatio = Height / mapExtent.Height;
                        Point logicalPoint = dp.LogicalCoordinate;

                        Point pixel = new Point(logicalPoint.X * xMaxToCurrRatio * mapExtent.Width, logicalPoint.Y * yMaxToCurrRatio * mapExtent.Height);

                        SetLeft(dp, pixel.X - dp.Anchor.X);
                        SetTop(dp, pixel.Y - dp.Anchor.Y);

                        break;
                    }
            }
        }

        private void UpdatePathData(PathControlBase pathGeometry)
        {
            var pathData = new PathGeometry();
            var pf = new PathFigure();
            int idx = 0;

            //Create an optimized calculation for the points in the loop.
            if (pathGeometry.Locations != null)
            {
                Size mapExtent = MapInstance.MapExtentPixelSize;
                double xMaxToCurrRatio = Width / mapExtent.Width;
                double yMaxToCurrRatio = Height / mapExtent.Height;

                foreach (Point logicalPoint in pathGeometry.LogicalCoordinates)
                {
                    //Add pixels relative to Layer size.
                    Point pixel;
                    switch (UpdateMode)
                    {
                        case UpdateModes.TransformUpdate:
                            {
                                pixel = new Point(logicalPoint.X * xMaxToCurrRatio * mapExtent.Width, logicalPoint.Y * yMaxToCurrRatio * mapExtent.Height);
                                break;
                            }
                        case UpdateModes.PanOnlyUpdate:
                            {
                                pixel = new Point(logicalPoint.X * mapExtent.Width, logicalPoint.Y * mapExtent.Height);
                                break;
                            }
                        case UpdateModes.ElementUpdate:
                            {
                                pixel = MapInstance.CoordHelper.LogicalToPixel(logicalPoint);
                                break;
                            }
                        default:
                            {
                                pixel = new Point();
                                break;
                            }
                    }

                    if (idx == 0)
                    {
                        pf.StartPoint = pixel;
                    }
                    else
                    {
                        var ls = new LineSegment { Point = pixel };
                        pf.Segments.Add(ls);
                    }
                    idx++;
                }
            }

            pf.IsClosed = pathGeometry.IsClosed;
            pathData.Figures.Add(pf);

            pathGeometry.PathData = pathData;
        }

        protected static void HideShape(GeometryControlBase<IGeometry> item)
        {
            item.InView = false;

            if (item is IPoint)
            {
                SetLeft(item, -1000);
                SetTop(item, -1000);
            }

            //if (item is PathBase)
            //{
            //    item.Visibility = Visibility.Collapsed;
            //}

            item.Visibility = Visibility.Collapsed;
        }

        protected void ShowShape(GeometryControlBase<IGeometry> item)
        {
            item.InView = true;

            if (item is IPoint)
            {
                UpdateChildLocation(item);
            }

            //if (shape is PathBase)
            //{
            //    if (shape.IsVisible) shape.Visibility = Visibility.Visible;
            //    UpdateChildLocation(shape);
            //}

            if (item.IsVisible) item.Visibility = Visibility.Visible;
            UpdateChildLocation(item);
        }


        #endregion
    }
}